/*
 * Copyright (C) 2010 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.airbnb.lottie.parser.moshi;

import okio.Buffer;
import okio.BufferedSource;
import okio.ByteString;
import org.jetbrains.annotations.Nullable;

import java.io.EOFException;
import java.io.IOException;

final class JsonUtf8Reader extends JsonReader {
    private static final long MIN_INCOMPLETE_INTEGER = Long.MIN_VALUE / 10;

    private static final ByteString SINGLE_QUOTE_OR_SLASH = ByteString.encodeUtf8("'\\");

    private static final ByteString DOUBLE_QUOTE_OR_SLASH = ByteString.encodeUtf8("\"\\");

    private static final ByteString UNQUOTED_STRING_TERMINALS = ByteString.encodeUtf8("{}[]:, \n\t\r\f/\\;#=");

    private static final ByteString LINEFEED_OR_CARRIAGE_RETURN = ByteString.encodeUtf8("\n\r");

    private static final ByteString CLOSING_BLOCK_COMMENT = ByteString.encodeUtf8("*/");

    private static final int PEEKED_NONE = 0;

    private static final int PEEKED_BEGIN_OBJECT = 1;

    private static final int PEEKED_END_OBJECT = 2;

    private static final int PEEKED_BEGIN_ARRAY = 3;

    private static final int PEEKED_END_ARRAY = 4;

    private static final int PEEKED_TRUE = 5;

    private static final int PEEKED_FALSE = 6;

    private static final int PEEKED_NULL = 7;

    private static final int PEEKED_SINGLE_QUOTED = 8;

    private static final int PEEKED_DOUBLE_QUOTED = 9;

    private static final int PEEKED_UNQUOTED = 10;

    /**
     * When this is returned, the string value is stored in peekedString.
     */
    private static final int PEEKED_BUFFERED = 11;

    private static final int PEEKED_SINGLE_QUOTED_NAME = 12;

    private static final int PEEKED_DOUBLE_QUOTED_NAME = 13;

    private static final int PEEKED_UNQUOTED_NAME = 14;

    private static final int PEEKED_BUFFERED_NAME = 15;

    /**
     * When this is returned, the integer value is stored in peekedLong.
     */
    private static final int PEEKED_LONG = 16;

    private static final int PEEKED_NUMBER = 17;

    private static final int PEEKED_EOF = 18;

    /* State machine when parsing numbers */
    private static final int NUMBER_CHAR_NONE = 0;

    private static final int NUMBER_CHAR_SIGN = 1;

    private static final int NUMBER_CHAR_DIGIT = 2;

    private static final int NUMBER_CHAR_DECIMAL = 3;

    private static final int NUMBER_CHAR_FRACTION_DIGIT = 4;

    private static final int NUMBER_CHAR_EXP_E = 5;

    private static final int NUMBER_CHAR_EXP_SIGN = 6;

    private static final int NUMBER_CHAR_EXP_DIGIT = 7;

    /**
     * The input JSON.
     */
    private final BufferedSource source;

    private final Buffer buffer;

    private int peeked = PEEKED_NONE;

    /**
     * A peeked value that was composed entirely of digits with an optional
     * leading dash. Positive values may not have a leading 0.
     */
    private long peekedLong;

    /**
     * The number of characters in a peeked number literal.
     */
    private int peekedNumberLength;

    /**
     * A peeked string that should be parsed on the next double, long or string.
     * This is populated before a numeric value is parsed and used if that parsing
     * fails.
     */
    private @Nullable String peekedString;

    JsonUtf8Reader(BufferedSource source) {
        if (source == null) {
            throw new NullPointerException("source == null");
        }
        this.source = source;
      // Don't use source.getBuffer(). Because IDE use old version okio instead of your own okio.
      buffer = source.getBuffer();
        pushScope(JsonScope.EMPTY_DOCUMENT);
    }

    @Override
    public void beginArray() throws IOException {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        if (p == PEEKED_BEGIN_ARRAY) {
            pushScope(JsonScope.EMPTY_ARRAY);
            pathIndices[stackSize - 1] = 0;
            peeked = PEEKED_NONE;
        } else {
            throw new JsonDataException("Expected BEGIN_ARRAY but was " + peek() + " at path " + getPath());
        }
    }

    @Override
    public void endArray() throws IOException {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        if (p == PEEKED_END_ARRAY) {
            stackSize--;
            pathIndices[stackSize - 1]++;
            peeked = PEEKED_NONE;
        } else {
            throw new JsonDataException("Expected END_ARRAY but was " + peek() + " at path " + getPath());
        }
    }

    @Override
    public void beginObject() throws IOException {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        if (p == PEEKED_BEGIN_OBJECT) {
            pushScope(JsonScope.EMPTY_OBJECT);
            peeked = PEEKED_NONE;
        } else {
            throw new JsonDataException("Expected BEGIN_OBJECT but was " + peek() + " at path " + getPath());
        }
    }

    @Override
    public void endObject() throws IOException {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        if (p == PEEKED_END_OBJECT) {
            stackSize--;
            pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected!
            pathIndices[stackSize - 1]++;
            peeked = PEEKED_NONE;
        } else {
            throw new JsonDataException("Expected END_OBJECT but was " + peek() + " at path " + getPath());
        }
    }

    @Override
    public boolean hasNext() throws IOException {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY && p != PEEKED_EOF;
    }

    @Override
    public Token peek() throws IOException {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }

        switch (p) {
            case PEEKED_BEGIN_OBJECT:
                return Token.BEGIN_OBJECT;
            case PEEKED_END_OBJECT:
                return Token.END_OBJECT;
            case PEEKED_BEGIN_ARRAY:
                return Token.BEGIN_ARRAY;
            case PEEKED_END_ARRAY:
                return Token.END_ARRAY;
            case PEEKED_SINGLE_QUOTED_NAME:
            case PEEKED_DOUBLE_QUOTED_NAME:
            case PEEKED_UNQUOTED_NAME:
            case PEEKED_BUFFERED_NAME:
                return Token.NAME;
            case PEEKED_TRUE:
            case PEEKED_FALSE:
                return Token.BOOLEAN;
            case PEEKED_NULL:
                return Token.NULL;
            case PEEKED_SINGLE_QUOTED:
            case PEEKED_DOUBLE_QUOTED:
            case PEEKED_UNQUOTED:
            case PEEKED_BUFFERED:
                return Token.STRING;
            case PEEKED_LONG:
            case PEEKED_NUMBER:
                return Token.NUMBER;
            case PEEKED_EOF:
                return Token.END_DOCUMENT;
            default:
                throw new AssertionError();
        }
    }

    private int doPeek() throws IOException {
        int peekStack = scopes[stackSize - 1];
        if (peekStack == JsonScope.EMPTY_ARRAY) {
            scopes[stackSize - 1] = JsonScope.NONEMPTY_ARRAY;
        } else if (peekStack == JsonScope.NONEMPTY_ARRAY) {
            // Look for a comma before the next element.
            int c = nextNonWhitespace(true);
            buffer.readByte(); // consume ']' or ','.
            switch (c) {
                case ']':
                    return peeked = PEEKED_END_ARRAY;
                case ';':
                    checkLenient(); // fall-through
                case ',':
                    break;
                default:
                    throw syntaxError("Unterminated array");
            }
        } else if (peekStack == JsonScope.EMPTY_OBJECT || peekStack == JsonScope.NONEMPTY_OBJECT) {
            scopes[stackSize - 1] = JsonScope.DANGLING_NAME;
            // Look for a comma before the next element.
            if (peekStack == JsonScope.NONEMPTY_OBJECT) {
                int c = nextNonWhitespace(true);
                buffer.readByte(); // Consume '}' or ','.
                switch (c) {
                    case '}':
                        return peeked = PEEKED_END_OBJECT;
                    case ';':
                        checkLenient(); // fall-through
                    case ',':
                        break;
                    default:
                        throw syntaxError("Unterminated object");
                }
            }
            int c = nextNonWhitespace(true);
            switch (c) {
                case '"':
                    buffer.readByte(); // consume the '\"'.
                    return peeked = PEEKED_DOUBLE_QUOTED_NAME;
                case '\'':
                    buffer.readByte(); // consume the '\''.
                    checkLenient();
                    return peeked = PEEKED_SINGLE_QUOTED_NAME;
                case '}':
                    if (peekStack != JsonScope.NONEMPTY_OBJECT) {
                        buffer.readByte(); // consume the '}'.
                        return peeked = PEEKED_END_OBJECT;
                    } else {
                        throw syntaxError("Expected name");
                    }
                default:
                    checkLenient();
                    if (isLiteral((char) c)) {
                        return peeked = PEEKED_UNQUOTED_NAME;
                    } else {
                        throw syntaxError("Expected name");
                    }
            }
        } else if (peekStack == JsonScope.DANGLING_NAME) {
            scopes[stackSize - 1] = JsonScope.NONEMPTY_OBJECT;
            // Look for a colon before the value.
            int c = nextNonWhitespace(true);
            buffer.readByte(); // Consume ':'.
            switch (c) {
                case ':':
                    break;
                case '=':
                    checkLenient();
                    if (source.request(1) && buffer.getByte(0) == '>') {
                        buffer.readByte(); // Consume '>'.
                    }
                    break;
                default:
                    throw syntaxError("Expected ':'");
            }
        } else if (peekStack == JsonScope.EMPTY_DOCUMENT) {
            scopes[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT;
        } else if (peekStack == JsonScope.NONEMPTY_DOCUMENT) {
            int c = nextNonWhitespace(false);
            if (c == -1) {
                return peeked = PEEKED_EOF;
            } else {
                checkLenient();
            }
        } else if (peekStack == JsonScope.CLOSED) {
            throw new IllegalStateException("JsonReader is closed");
        }

        int c = nextNonWhitespace(true);
        switch (c) {
            case ']':
                if (peekStack == JsonScope.EMPTY_ARRAY) {
                    buffer.readByte(); // Consume ']'.
                    return peeked = PEEKED_END_ARRAY;
                }
                // fall-through to handle ",]"
            case ';':
            case ',':
                // In lenient mode, a 0-length literal in an array means 'null'.
                if (peekStack == JsonScope.EMPTY_ARRAY || peekStack == JsonScope.NONEMPTY_ARRAY) {
                    checkLenient();
                    return peeked = PEEKED_NULL;
                } else {
                    throw syntaxError("Unexpected value");
                }
            case '\'':
                checkLenient();
                buffer.readByte(); // Consume '\''.
                return peeked = PEEKED_SINGLE_QUOTED;
            case '"':
                buffer.readByte(); // Consume '\"'.
                return peeked = PEEKED_DOUBLE_QUOTED;
            case '[':
                buffer.readByte(); // Consume '['.
                return peeked = PEEKED_BEGIN_ARRAY;
            case '{':
                buffer.readByte(); // Consume '{'.
                return peeked = PEEKED_BEGIN_OBJECT;
            default:
        }

        int result = peekKeyword();
        if (result != PEEKED_NONE) {
            return result;
        }

        result = peekNumber();
        if (result != PEEKED_NONE) {
            return result;
        }

        if (!isLiteral(buffer.getByte(0))) {
            throw syntaxError("Expected value");
        }

        checkLenient();
        return peeked = PEEKED_UNQUOTED;
    }

    private int peekKeyword() throws IOException {
        // Figure out which keyword we're matching against by its first character.
        byte c = buffer.getByte(0);
        String keyword;
        String keywordUpper;
        int peeking;
        if (c == 't' || c == 'T') {
            keyword = "true";
            keywordUpper = "TRUE";
            peeking = PEEKED_TRUE;
        } else if (c == 'f' || c == 'F') {
            keyword = "false";
            keywordUpper = "FALSE";
            peeking = PEEKED_FALSE;
        } else if (c == 'n' || c == 'N') {
            keyword = "null";
            keywordUpper = "NULL";
            peeking = PEEKED_NULL;
        } else {
            return PEEKED_NONE;
        }

        // Confirm that chars [1..length) match the keyword.
        int length = keyword.length();
        for (int i = 1; i < length; i++) {
            if (!source.request(i + 1)) {
                return PEEKED_NONE;
            }
            c = buffer.getByte(i);
            if (c != keyword.charAt(i) && c != keywordUpper.charAt(i)) {
                return PEEKED_NONE;
            }
        }

        if (source.request(length + 1) && isLiteral(buffer.getByte(length))) {
            return PEEKED_NONE; // Don't match trues, falsey or nullsoft!
        }

        // We've found the keyword followed either by EOF or by a non-literal character.
        buffer.skip(length);
        return peeked = peeking;
    }

    private int peekNumber() throws IOException {
        long value = 0; // Negative to accommodate Long.MIN_VALUE more easily.
        boolean negative = false;
        boolean fitsInLong = true;
        int last = NUMBER_CHAR_NONE;

        int i = 0;

        charactersOfNumber:
        for (; true; i++) {
            if (!source.request(i + 1)) {
                break;
            }

            byte c = buffer.getByte(i);
            switch (c) {
                case '-':
                    if (last == NUMBER_CHAR_NONE) {
                        negative = true;
                        last = NUMBER_CHAR_SIGN;
                        continue;
                    } else if (last == NUMBER_CHAR_EXP_E) {
                        last = NUMBER_CHAR_EXP_SIGN;
                        continue;
                    }
                    return PEEKED_NONE;

                case '+':
                    if (last == NUMBER_CHAR_EXP_E) {
                        last = NUMBER_CHAR_EXP_SIGN;
                        continue;
                    }
                    return PEEKED_NONE;

                case 'e':
                case 'E':
                    if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT) {
                        last = NUMBER_CHAR_EXP_E;
                        continue;
                    }
                    return PEEKED_NONE;

                case '.':
                    if (last == NUMBER_CHAR_DIGIT) {
                        last = NUMBER_CHAR_DECIMAL;
                        continue;
                    }
                    return PEEKED_NONE;

                default:
                    if (c < '0' || c > '9') {
                        if (!isLiteral(c)) {
                            break charactersOfNumber;
                        }
                        return PEEKED_NONE;
                    }
                    if (last == NUMBER_CHAR_SIGN || last == NUMBER_CHAR_NONE) {
                        value = -(c - '0');
                        last = NUMBER_CHAR_DIGIT;
                    } else if (last == NUMBER_CHAR_DIGIT) {
                        if (value == 0) {
                            return PEEKED_NONE; // Leading '0' prefix is not allowed (since it could be octal).
                        }
                        long newValue = value * 10 - (c - '0');
                        fitsInLong &= value > MIN_INCOMPLETE_INTEGER || (value == MIN_INCOMPLETE_INTEGER
                            && newValue < value);
                        value = newValue;
                    } else if (last == NUMBER_CHAR_DECIMAL) {
                        last = NUMBER_CHAR_FRACTION_DIGIT;
                    } else if (last == NUMBER_CHAR_EXP_E || last == NUMBER_CHAR_EXP_SIGN) {
                        last = NUMBER_CHAR_EXP_DIGIT;
                    }
            }
        }

        // We've read a complete number. Decide if it's a PEEKED_LONG or a PEEKED_NUMBER.
        if (last == NUMBER_CHAR_DIGIT && fitsInLong && (value != Long.MIN_VALUE || negative) && (value != 0
            || !negative)) {
            peekedLong = negative ? value : -value;
            buffer.skip(i);
            return peeked = PEEKED_LONG;
        } else if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT || last == NUMBER_CHAR_EXP_DIGIT) {
            peekedNumberLength = i;
            return peeked = PEEKED_NUMBER;
        } else {
            return PEEKED_NONE;
        }
    }

    private boolean isLiteral(int c) throws IOException {
        switch (c) {
            case '/':
            case '\\':
            case ';':
            case '#':
            case '=':
                checkLenient(); // fall-through
            case '{':
            case '}':
            case '[':
            case ']':
            case ':':
            case ',':
            case ' ':
            case '\t':
            case '\f':
            case '\r':
            case '\n':
                return false;
            default:
                return true;
        }
    }

    @Override
    public String nextName() throws IOException {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        String result;
        if (p == PEEKED_UNQUOTED_NAME) {
            result = nextUnquotedValue();
        } else if (p == PEEKED_DOUBLE_QUOTED_NAME) {
            result = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH);
        } else if (p == PEEKED_SINGLE_QUOTED_NAME) {
            result = nextQuotedValue(SINGLE_QUOTE_OR_SLASH);
        } else if (p == PEEKED_BUFFERED_NAME) {
            result = peekedString;
        } else {
            throw new JsonDataException("Expected a name but was " + peek() + " at path " + getPath());
        }
        peeked = PEEKED_NONE;
        pathNames[stackSize - 1] = result;
        return result;
    }

    @Override
    public int selectName(Options options) throws IOException {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        if (p < PEEKED_SINGLE_QUOTED_NAME || p > PEEKED_BUFFERED_NAME) {
            return -1;
        }
        if (p == PEEKED_BUFFERED_NAME) {
            return findName(peekedString, options);
        }

        int result = source.select(options.doubleQuoteSuffix);
        if (result != -1) {
            peeked = PEEKED_NONE;
            pathNames[stackSize - 1] = options.strings[result];

            return result;
        }

        // The next name may be unnecessary escaped. Save the last recorded path name, so that we
        // can restore the peek state in case we fail to find a match.
        String lastPathName = pathNames[stackSize - 1];

        String nextName = nextName();
        result = findName(nextName, options);

        if (result == -1) {
            peeked = PEEKED_BUFFERED_NAME;
            peekedString = nextName;
            // We can't push the path further, make it seem like nothing happened.
            pathNames[stackSize - 1] = lastPathName;
        }

        return result;
    }

    @Override
    public void skipName() throws IOException {
        if (failOnUnknown) {
            throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath());
        }
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        if (p == PEEKED_UNQUOTED_NAME) {
            skipUnquotedValue();
        } else if (p == PEEKED_DOUBLE_QUOTED_NAME) {
            skipQuotedValue(DOUBLE_QUOTE_OR_SLASH);
        } else if (p == PEEKED_SINGLE_QUOTED_NAME) {
            skipQuotedValue(SINGLE_QUOTE_OR_SLASH);
        } else if (p != PEEKED_BUFFERED_NAME) {
            throw new JsonDataException("Expected a name but was " + peek() + " at path " + getPath());
        }
        peeked = PEEKED_NONE;
        pathNames[stackSize - 1] = "null";
    }

    /**
     * If {@code name} is in {@code options} this consumes it and returns its index.
     * Otherwise this returns -1 and no name is consumed.
     * @param name in String
     * @param options of type options
     * @return index for given name else -1
     */
    private int findName(String name, Options options) {
        for (int i = 0, size = options.strings.length; i < size; i++) {
            if (name.equals(options.strings[i])) {
                peeked = PEEKED_NONE;
                pathNames[stackSize - 1] = name;

                return i;
            }
        }
        return -1;
    }

    @Override
    public String nextString() throws IOException {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        String result;
        if (p == PEEKED_UNQUOTED) {
            result = nextUnquotedValue();
        } else if (p == PEEKED_DOUBLE_QUOTED) {
            result = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH);
        } else if (p == PEEKED_SINGLE_QUOTED) {
            result = nextQuotedValue(SINGLE_QUOTE_OR_SLASH);
        } else if (p == PEEKED_BUFFERED) {
            result = peekedString;
            peekedString = null;
        } else if (p == PEEKED_LONG) {
            result = Long.toString(peekedLong);
        } else if (p == PEEKED_NUMBER) {
            result = buffer.readUtf8(peekedNumberLength);
        } else {
            throw new JsonDataException("Expected a string but was " + peek() + " at path " + getPath());
        }
        peeked = PEEKED_NONE;
        pathIndices[stackSize - 1]++;
        return result;
    }

    /**
     * If {@code string} is in {@code options} this consumes it and returns its index.
     * Otherwise this returns -1 and no string is consumed.
     * @param string in String
     * @param options of type options
     * @return index for given string else -1
     */
    private int findString(String string, Options options) {
        for (int i = 0, size = options.strings.length; i < size; i++) {
            if (string.equals(options.strings[i])) {
                peeked = PEEKED_NONE;
                pathIndices[stackSize - 1]++;

                return i;
            }
        }
        return -1;
    }

    @Override
    public boolean nextBoolean() throws IOException {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        if (p == PEEKED_TRUE) {
            peeked = PEEKED_NONE;
            pathIndices[stackSize - 1]++;
            return true;
        } else if (p == PEEKED_FALSE) {
            peeked = PEEKED_NONE;
            pathIndices[stackSize - 1]++;
            return false;
        }
        throw new JsonDataException("Expected a boolean but was " + peek() + " at path " + getPath());
    }

    @Override
    public double nextDouble() throws IOException {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }

        if (p == PEEKED_LONG) {
            peeked = PEEKED_NONE;
            pathIndices[stackSize - 1]++;
            return (double) peekedLong;
        }

        if (p == PEEKED_NUMBER) {
            peekedString = buffer.readUtf8(peekedNumberLength);
        } else if (p == PEEKED_DOUBLE_QUOTED) {
            peekedString = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH);
        } else if (p == PEEKED_SINGLE_QUOTED) {
            peekedString = nextQuotedValue(SINGLE_QUOTE_OR_SLASH);
        } else if (p == PEEKED_UNQUOTED) {
            peekedString = nextUnquotedValue();
        } else if (p != PEEKED_BUFFERED) {
            throw new JsonDataException("Expected a double but was " + peek() + " at path " + getPath());
        }

        peeked = PEEKED_BUFFERED;
        double result;
        try {
            result = Double.parseDouble(peekedString);
        } catch (NumberFormatException e) {
            throw new JsonDataException("Expected a double but was " + peekedString + " at path " + getPath());
        }
        if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) {
            throw new JsonEncodingException("JSON forbids NaN and infinities: " + result + " at path " + getPath());
        }
        peekedString = null;
        peeked = PEEKED_NONE;
        pathIndices[stackSize - 1]++;
        return result;
    }

    /**
     * Returns the string up to but not including {@code quote}, unescaping any character escape
     * sequences encountered along the way. The opening quote should have already been read. This
     * consumes the closing quote, but does not include it in the returned string.
     * @param runTerminator of type ByteString
     * @throws IOException if any unicode escape sequences are malformed.
     * @return Returns the string up to but not including {@code quote}
     */
    private String nextQuotedValue(ByteString runTerminator) throws IOException {
        StringBuilder builder = null;
        while (true) {
            long index = source.indexOfElement(runTerminator);
          if (index == -1L) {
            throw syntaxError("Unterminated string");
          }

            // If we've got an escape character, we're going to need a string builder.
            if (buffer.getByte(index) == '\\') {
              if (builder == null) {
                builder = new StringBuilder();
              }
                builder.append(buffer.readUtf8(index));
                buffer.readByte(); // '\'
                builder.append(readEscapeCharacter());
                continue;
            }

            // If it isn't the escape character, it's the quote. Return the string.
            if (builder == null) {
                String result = buffer.readUtf8(index);
                buffer.readByte(); // Consume the quote character.
                return result;
            } else {
                builder.append(buffer.readUtf8(index));
                buffer.readByte(); // Consume the quote character.
                return builder.toString();
            }
        }
    }

    /**
     * Returns an unquoted value as a string.
     * @throws IOException
     * @return an unquoted value as a string.
     */
    private String nextUnquotedValue() throws IOException {
        long i = source.indexOfElement(UNQUOTED_STRING_TERMINALS);
        return i != -1 ? buffer.readUtf8(i) : buffer.readUtf8();
    }

    private void skipQuotedValue(ByteString runTerminator) throws IOException {
        while (true) {
            long index = source.indexOfElement(runTerminator);
          if (index == -1L) {
            throw syntaxError("Unterminated string");
          }

            if (buffer.getByte(index) == '\\') {
                buffer.skip(index + 1);
                readEscapeCharacter();
            } else {
                buffer.skip(index + 1);
                return;
            }
        }
    }

    private void skipUnquotedValue() throws IOException {
        long i = source.indexOfElement(UNQUOTED_STRING_TERMINALS);
        buffer.skip(i != -1L ? i : buffer.size());
    }

    @Override
    public int nextInt() throws IOException {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }

        int result;
        if (p == PEEKED_LONG) {
            result = (int) peekedLong;
            if (peekedLong != result) { // Make sure no precision was lost casting to 'int'.
                throw new JsonDataException("Expected an int but was " + peekedLong + " at path " + getPath());
            }
            peeked = PEEKED_NONE;
            pathIndices[stackSize - 1]++;
            return result;
        }

        if (p == PEEKED_NUMBER) {
            peekedString = buffer.readUtf8(peekedNumberLength);
        } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_SINGLE_QUOTED) {
            peekedString = p == PEEKED_DOUBLE_QUOTED
                ? nextQuotedValue(DOUBLE_QUOTE_OR_SLASH)
                : nextQuotedValue(SINGLE_QUOTE_OR_SLASH);
            try {
                result = Integer.parseInt(peekedString);
                peeked = PEEKED_NONE;
                pathIndices[stackSize - 1]++;
                return result;
            } catch (NumberFormatException ignored) {
                // Fall back to parse as a double below.
            }
        } else if (p != PEEKED_BUFFERED) {
            throw new JsonDataException("Expected an int but was " + peek() + " at path " + getPath());
        }

        peeked = PEEKED_BUFFERED;
        double asDouble;
        try {
            asDouble = Double.parseDouble(peekedString);
        } catch (NumberFormatException e) {
            throw new JsonDataException("Expected an int but was " + peekedString + " at path " + getPath());
        }
        result = (int) asDouble;
        if (result != asDouble) { // Make sure no precision was lost casting to 'int'.
            throw new JsonDataException("Expected an int but was " + peekedString + " at path " + getPath());
        }
        peekedString = null;
        peeked = PEEKED_NONE;
        pathIndices[stackSize - 1]++;
        return result;
    }

    @Override
    public void close() throws IOException {
        peeked = PEEKED_NONE;
        scopes[0] = JsonScope.CLOSED;
        stackSize = 1;
        buffer.clear();
        source.close();
    }

    @Override
    public void skipValue() throws IOException {
        if (failOnUnknown) {
            throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath());
        }
        int count = 0;
        do {
            int p = peeked;
            if (p == PEEKED_NONE) {
                p = doPeek();
            }

            if (p == PEEKED_BEGIN_ARRAY) {
                pushScope(JsonScope.EMPTY_ARRAY);
                count++;
            } else if (p == PEEKED_BEGIN_OBJECT) {
                pushScope(JsonScope.EMPTY_OBJECT);
                count++;
            } else if (p == PEEKED_END_ARRAY) {
                count--;
                if (count < 0) {
                    throw new JsonDataException("Expected a value but was " + peek() + " at path " + getPath());
                }
                stackSize--;
            } else if (p == PEEKED_END_OBJECT) {
                count--;
                if (count < 0) {
                    throw new JsonDataException("Expected a value but was " + peek() + " at path " + getPath());
                }
                stackSize--;
            } else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) {
                skipUnquotedValue();
            } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) {
                skipQuotedValue(DOUBLE_QUOTE_OR_SLASH);
            } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) {
                skipQuotedValue(SINGLE_QUOTE_OR_SLASH);
            } else if (p == PEEKED_NUMBER) {
                buffer.skip(peekedNumberLength);
            } else if (p == PEEKED_EOF) {
                throw new JsonDataException("Expected a value but was " + peek() + " at path " + getPath());
            }
            peeked = PEEKED_NONE;
        } while (count != 0);

        pathIndices[stackSize - 1]++;
        pathNames[stackSize - 1] = "null";
    }

    /**
     * Returns the next character in the stream that is neither whitespace nor a
     * part of a comment. When this returns, the returned character is always at
     * {@code buffer.getByte(0)}.
     * @param throwOnEof takes boolean value
     * @throws IOException
     * @return the next character in the stream that is neither whitespace nor a
     * part of a comment
     */
    private int nextNonWhitespace(boolean throwOnEof) throws IOException {
        /*
         * This code uses ugly local variables 'p' and 'l' representing the 'pos'
         * and 'limit' fields respectively. Using locals rather than fields saves
         * a few field reads for each whitespace character in a pretty-printed
         * document, resulting in a 5% speedup. We need to flush 'p' to its field
         * before any (potentially indirect) call to fillBuffer() and reread both
         * 'p' and 'l' after any (potentially indirect) call to the same method.
         */
        int p = 0;
        while (source.request(p + 1)) {
            int c = buffer.getByte(p++);
            if (c == '\n' || c == ' ' || c == '\r' || c == '\t') {
                continue;
            }

            buffer.skip(p - 1);
            if (c == '/') {
                if (!source.request(2)) {
                    return c;
                }

                checkLenient();
                byte peek = buffer.getByte(1);
                switch (peek) {
                    case '*':
                        // skip a /* c-style comment */
                        buffer.readByte(); // '/'
                        buffer.readByte(); // '*'
                        if (!skipToEndOfBlockComment()) {
                            throw syntaxError("Unterminated comment");
                        }
                        p = 0;
                        continue;

                    case '/':
                        // skip a // end-of-line comment
                        buffer.readByte(); // '/'
                        buffer.readByte(); // '/'
                        skipToEndOfLine();
                        p = 0;
                        continue;

                    default:
                        return c;
                }
            } else if (c == '#') {
                // Skip a # hash end-of-line comment. The JSON RFC doesn't specify this behaviour, but it's
                // required to parse existing documents.
                checkLenient();
                skipToEndOfLine();
                p = 0;
            } else {
                return c;
            }
        }
        if (throwOnEof) {
            throw new EOFException("End of input");
        } else {
            return -1;
        }
    }

    private void checkLenient() throws IOException {
        if (!lenient) {
            throw syntaxError("Use JsonReader.setLenient(true) to accept malformed JSON");
        }
    }

    /**
     * Advances the position until after the next newline character. If the line
     * is terminated by "\r\n", the '\n' must be consumed as whitespace by the
     * caller.
     * @throws IOException
     */
    private void skipToEndOfLine() throws IOException {
        long index = source.indexOfElement(LINEFEED_OR_CARRIAGE_RETURN);
        buffer.skip(index != -1 ? index + 1 : buffer.size());
    }

    /**
     * Skips through the next closing block comment.
     * @throws IOException
     * @return boolean
     */
    private boolean skipToEndOfBlockComment() throws IOException {
        long index = source.indexOf(CLOSING_BLOCK_COMMENT);
        boolean found = index != -1;
        buffer.skip(found ? index + CLOSING_BLOCK_COMMENT.size() : buffer.size());
        return found;
    }

    @Override
    public String toString() {
        return "JsonReader(" + source + ")";
    }

    /**
     * Unescapes the character identified by the character or characters that immediately follow a
     * backslash. The backslash '\' should have already been read. This supports both unicode escapes
     * "u000A" and two-character escapes "\n".
     *
     * @throws IOException if any unicode escape sequences are malformed.
     * @return char
     */
    private char readEscapeCharacter() throws IOException {
        if (!source.request(1)) {
            throw syntaxError("Unterminated escape sequence");
        }

        byte escaped = buffer.readByte();
        switch (escaped) {
            case 'u':
                if (!source.request(4)) {
                    throw new EOFException("Unterminated escape sequence at path " + getPath());
                }
                // Equivalent to Integer.parseInt(stringPool.get(buffer, pos, 4), 16);
                char result = 0;
                for (int i = 0, end = i + 4; i < end; i++) {
                    byte c = buffer.getByte(i);
                    result <<= 4;
                    if (c >= '0' && c <= '9') {
                        result += (c - '0');
                    } else if (c >= 'a' && c <= 'f') {
                        result += (c - 'a' + 10);
                    } else if (c >= 'A' && c <= 'F') {
                        result += (c - 'A' + 10);
                    } else {
                        throw syntaxError("\\u" + buffer.readUtf8(4));
                    }
                }
                buffer.skip(4);
                return result;

            case 't':
                return '\t';

            case 'b':
                return '\b';

            case 'n':
                return '\n';

            case 'r':
                return '\r';

            case 'f':
                return '\f';

            case '\n':
            case '\'':
            case '"':
            case '\\':
            case '/':
                return (char) escaped;

            default:
              if (!lenient) {
                throw syntaxError("Invalid escape sequence: \\" + (char) escaped);
              }
                return (char) escaped;
        }
    }

}